import os
import re
import functools
import zipfile
import tempfile
from typing import List, Optional
from uuid import UUID
from datetime import datetime
from fastapi import UploadFile, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
import anyio

from crud import attachment as attachment_crud
from crud import complianceItem as compliance_item_crud
from crud import assessmentPlan as assessment_plan_crud
from schemas.attachment import AttachmentCreate, AttachmentResponse
from log import log_info, log_error

# 允许的文件类型及其对应的MIME类型前缀
IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'}
DOCUMENT_EXTENSIONS = {'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'}

# 合并所有允许的扩展名（不包含前缀）
ALLOWED_EXTENSIONS = IMAGE_EXTENSIONS.union(DOCUMENT_EXTENSIONS)

# 获取带前缀的扩展名
def get_extension_with_prefix(ext: str) -> str:
    """获取带类型前缀的扩展名"""
    ext = ext.lower()
    if ext in IMAGE_EXTENSIONS:
        return f'image/{ext}'
    elif ext in DOCUMENT_EXTENSIONS:
        return f'file/{ext}'
    return ext

# 文件大小限制（20MB）
MAX_FILE_SIZE: int = 20 * 1024 * 1024

def is_allowed_file(filename: str) -> bool:
    """检查文件类型是否允许"""
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def sanitize_filename(filename: str) -> str:
    """
    清理文件名，保留中文和英文字符，移除特殊字符
    
    Args:
        filename: 原始文件名
        
    Returns:
        清理后的文件名
        
    示例:
        >>> sanitize_filename("测试文件(1).jpg")
        '测试文件1.jpg'
        >>> sanitize_filename("安全检查报告#2023.pdf")
        '安全检查报告2023.pdf'
        >>> sanitize_filename("风险评估_结果分析.docx")
        '风险评估_结果分析.docx'
    """
    # 移除路径分隔符，只保留文件名
    filename = os.path.basename(filename)
    
    # 分离文件名和扩展名
    name, ext = os.path.splitext(filename)
    
    # 定义要保留的字符模式
    # \u4e00-\u9fff: 中文字符范围
    # \w: 字母、数字、下划线
    # \-: 连字符
    # \.: 点号
    pattern = r'[^\u4e00-\u9fff\w\-\.]'
    
    # 清理文件名（不包含扩展名）
    clean_name = re.sub(pattern, '', name)
    
    # 如果清理后的文件名为空，使用默认名称
    if not clean_name:
        clean_name = "unnamed"
    
    # 重新组合文件名和扩展名
    return clean_name + ext

async def get_file_storage_path(
    db: AsyncSession,
    compliance_item_id: UUID,
    filename: str
) -> str:
    """获取文件存储路径"""
    try:
        # 获取合规项信息
        compliance_item = await compliance_item_crud.get_compliance_item_by_id(db, compliance_item_id)
        if not compliance_item:
            raise HTTPException(status_code=404, detail="Compliance item not found")

        # 获取评估计划信息
        assessment_plan = compliance_item.assessment_plan

        # 清理并验证文件名
        safe_filename = sanitize_filename(filename)
        if not safe_filename:
            raise HTTPException(status_code=400, detail="文件名不合法")

        # 构建存储路径
        storage_path = os.path.join(
            "static",
            "compliance_attachment",
            str(assessment_plan.name),
            str(assessment_plan.assessment_type),
            str(compliance_item.compliance_type),
            str(compliance_item.title),
        )

        # 确保目录存在
        await anyio.to_thread.run_sync(functools.partial(os.makedirs, exist_ok=True), storage_path)

        # 生成文件路径
        file_path = os.path.join(storage_path, safe_filename)
        
        # 检查文件是否已存在
        if await anyio.to_thread.run_sync(os.path.exists, file_path):
            raise HTTPException(
                status_code=400,
                detail=f"文件名'{safe_filename}'已存在"
            )

        return file_path

    except HTTPException:
        raise
    except Exception as e:
        log_error(f"Unexpected error in get_file_storage_path: {str(e)}")
        raise HTTPException(status_code=500, detail="Internal server error while processing file path")

async def save_attachment(
    db: AsyncSession,
    file: UploadFile,
    compliance_item_id: UUID
) -> AttachmentResponse:
    """保存附件"""
    try:
        # 验证文件类型
        if not is_allowed_file(file.filename):
            raise HTTPException(
                status_code=400,
                detail=f"文件类型不合法，允许的文件类型: {', '.join(ALLOWED_EXTENSIONS)}"
            )

        # 读取文件内容
        content = await file.read()
        
        # 验证文件大小
        if len(content) > MAX_FILE_SIZE:
            raise HTTPException(
                status_code=400,
                detail=f"文件大小超过限制，允许的文件大小: {MAX_FILE_SIZE // 1024 // 1024}MB"
            )

        # 获取文件存储路径
        file_path = await get_file_storage_path(db, compliance_item_id, file.filename)

        try:
            # 保存文件
            def _write_file_sync():
                with open(file_path, "wb") as f:
                    f.write(content)
            await anyio.to_thread.run_sync(_write_file_sync)

            # 获取文件扩展名并添加前缀
            file_ext = os.path.splitext(file.filename)[1].lower()
            if file_ext.startswith('.'):
                file_ext = file_ext[1:]  # 移除开头的点
                
            # 获取带前缀的扩展名
            file_type = get_extension_with_prefix(file_ext)
            
            # 创建附件记录
            attachment_create = AttachmentCreate(
                compliance_item_id=compliance_item_id,
                name=os.path.basename(file_path),
                type=file_type,  # 存储带前缀的文件类型
                size=len(content)
            )

            db_attachment = await attachment_crud.create_attachment(
                db=db,
                attachment=attachment_create,
                file_path=file_path
            )

            log_info(f"Saved attachment {db_attachment.id} to {file_path}")
            return AttachmentResponse.model_validate(db_attachment)

        except Exception as e:
            # 如果数据库操作失败，删除已保存的文件
            if await anyio.to_thread.run_sync(os.path.exists, file_path):
                await anyio.to_thread.run_sync(os.remove, file_path)
            raise

    except Exception as e:
        log_error(f"Error saving attachment: {str(e)}")
        if isinstance(e, HTTPException):
            raise
        raise HTTPException(status_code=500, detail="Error saving attachment")

async def delete_attachment_with_file(
    db: AsyncSession,
    attachment_id: UUID
) -> bool:
    """删除附件及其对应的文件"""
    try:
        # 获取附件信息
        attachment = await attachment_crud.get_attachment(db, attachment_id)
        if not attachment:
            return False

        # 验证文件路径
        if not os.path.abspath(attachment.path).startswith(
            os.path.abspath("static/compliance_attachment")
        ):
            log_error(f"Invalid file path detected: {attachment.path}")
            raise HTTPException(status_code=400, detail="Invalid file path")

        # 删除文件
        try:
            if await anyio.to_thread.run_sync(os.path.exists, attachment.path):
                await anyio.to_thread.run_sync(os.remove, attachment.path)
        except OSError as e:
            log_error(f"Error deleting file {attachment.path}: {str(e)}")
            raise HTTPException(status_code=500, detail="Error deleting file")

        # 删除数据库记录
        result = await attachment_crud.delete_attachment(db, attachment_id)
        
        if result:
            log_info(f"Deleted attachment {attachment_id} and its file")
        return result

    except Exception as e:
        log_error(f"Error deleting attachment and file: {str(e)}")
        if isinstance(e, HTTPException):
            raise
        raise HTTPException(status_code=500, detail="Error deleting attachment")

async def get_attachments(
    db: AsyncSession,
    compliance_item_id: UUID
) -> List[AttachmentResponse]:
    """获取合规项的所有附件"""
    try:
        attachments = await attachment_crud.get_attachments_by_compliance_item(
            db, compliance_item_id
        )
        return [AttachmentResponse.model_validate(att) for att in attachments]
    except Exception as e:
        log_error(f"Error getting attachments: {str(e)}")
        raise HTTPException(status_code=500, detail="Error retrieving attachments")

async def get_attachment(
    db: AsyncSession,
    attachment_id: UUID
) -> Optional[AttachmentResponse]:
    """
    获取单个附件信息
    
    Args:
        db: 数据库会话
        attachment_id: 附件ID
        
    Returns:
        附件信息，如果不存在返回None
    """
    try:
        attachment = await attachment_crud.get_attachment(db, attachment_id)
        if attachment:
            log_info(f"Retrieved attachment: {attachment_id}")
        return attachment
    except Exception as e:
        log_error(f"Error retrieving attachment {attachment_id}: {str(e)}")
        raise HTTPException(status_code=500, detail="Error retrieving attachment")

async def get_attachments_by_assessment_plan(
    db: AsyncSession,
    assessment_plan_id: UUID
) -> List[AttachmentResponse]:
    """
    获取评估计划的所有附件
    
    Args:
        db: 数据库会话
        assessment_plan_id: 评估计划ID
        
    Returns:
        附件列表
    """
    try:
        # 验证评估计划是否存在
        assessment_plan = await assessment_plan_crud.get_assessment_plan_by_id(db, assessment_plan_id)
        if not assessment_plan:
            raise HTTPException(status_code=404, detail="评估计划不存在")
        
        # 获取所有附件
        attachments = await attachment_crud.get_attachments_by_assessment_plan(
            db, assessment_plan_id
        )
        return [AttachmentResponse.model_validate(att) for att in attachments]
    except HTTPException:
        raise
    except Exception as e:
        log_error(f"Error getting attachments for assessment plan: {str(e)}")
        raise HTTPException(status_code=500, detail="获取附件列表失败")


async def copy_existing_attachments(
    db: AsyncSession,
    compliance_item_id: UUID,
    attachment_ids: List[UUID]
) -> List[AttachmentResponse]:
    """
    复制已有的附件到新的检查项（创建新的文件和记录）
    
    Args:
        db: 数据库会话
        compliance_item_id: 目标检查项ID
        attachment_ids: 要复制的附件ID列表
        
    Returns:
        新创建的附件记录列表
    """
    try:
        # 验证目标检查项是否存在
        compliance_item = await compliance_item_crud.get_compliance_item_by_id(db, compliance_item_id)
        if not compliance_item:
            raise HTTPException(status_code=404, detail="检查项不存在")
        
        new_attachments = []
        
        for attachment_id in attachment_ids:
            # 获取源附件信息
            source_attachment = await attachment_crud.get_attachment(db, attachment_id)
            if not source_attachment:
                log_error(f"Attachment {attachment_id} not found, skipping")
                continue
            
            # 检查源文件是否存在
            if not await anyio.to_thread.run_sync(os.path.exists, source_attachment.path):
                log_error(f"Source file not found: {source_attachment.path}")
                continue
            
            try:
                # 读取源文件内容
                def _read_file_sync():
                    with open(source_attachment.path, 'rb') as f:
                        return f.read()
                
                file_content = await anyio.to_thread.run_sync(_read_file_sync)
                
                # 获取新的文件存储路径
                new_file_path = await get_file_storage_path(db, compliance_item_id, source_attachment.name)
                
                # 复制文件到新位置
                def _write_file_sync():
                    with open(new_file_path, 'wb') as f:
                        f.write(file_content)
                
                await anyio.to_thread.run_sync(_write_file_sync)
                
                # 创建新的附件记录
                attachment_create = AttachmentCreate(
                    compliance_item_id=compliance_item_id,
                    name=source_attachment.name,
                    type=source_attachment.type,
                    size=source_attachment.size
                )
                
                new_attachment = await attachment_crud.create_attachment(
                    db=db,
                    attachment=attachment_create,
                    file_path=new_file_path
                )
                
                new_attachments.append(AttachmentResponse.model_validate(new_attachment))
                log_info(f"Copied attachment {attachment_id} to compliance item {compliance_item_id} as new attachment {new_attachment.id}")
                
            except HTTPException as e:
                # 如果文件名已存在，跳过该文件
                if "已存在" in str(e.detail):
                    log_info(f"File {source_attachment.name} already exists in target, skipping")
                    continue
                raise
            except Exception as e:
                log_error(f"Error copying attachment {attachment_id}: {str(e)}")
                # 清理可能创建的文件
                if 'new_file_path' in locals() and await anyio.to_thread.run_sync(os.path.exists, new_file_path):
                    await anyio.to_thread.run_sync(os.remove, new_file_path)
                continue
        
        return new_attachments
        
    except HTTPException:
        raise
    except Exception as e:
        log_error(f"Error copying attachments: {str(e)}")
        raise HTTPException(status_code=500, detail="复制附件失败")


def _create_zip_file(source_dir: str, zip_path: str):
    """同步创建压缩文件的辅助函数"""
    with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
        for root, dirs, files in os.walk(source_dir):
            for file in files:
                file_path = os.path.join(root, file)
                # 保持相对路径结构
                arcname = os.path.relpath(file_path, source_dir)
                zipf.write(file_path, arcname)


async def export_assessment_plan_attachments(
    db: AsyncSession,
    assessment_plan_id: UUID
) -> str:
    """
    根据评估计划ID导出所有附件为压缩包
    
    Args:
        db: 数据库会话
        assessment_plan_id: 评估计划ID
        
    Returns:
        压缩包文件路径
        
    Raises:
        HTTPException: 当评估计划不存在或操作失败时
    """
    try:
        # 查询评估计划信息
        assessment_plan = await assessment_plan_crud.get_assessment_plan_by_id(db, assessment_plan_id)
        if not assessment_plan:
            raise HTTPException(status_code=404, detail="评估计划不存在")

        # 构建附件目录路径
        attachment_dir = os.path.join(
            "static",
            "compliance_attachment",
            str(assessment_plan.name),
            str(assessment_plan.assessment_type)
        )

        # 检查目录是否存在
        if not await anyio.to_thread.run_sync(os.path.isdir, attachment_dir):
            raise HTTPException(status_code=404, detail="附件目录不存在，请检查是否上传附件")

        # 创建压缩包文件名和路径
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        zip_filename = f"{assessment_plan.name}_{assessment_plan.assessment_type}_{timestamp}.zip"
        zip_path = os.path.join(tempfile.gettempdir(), zip_filename)

        # 执行压缩操作
        await anyio.to_thread.run_sync(_create_zip_file, attachment_dir, zip_path)
        
        log_info(f"Created attachment export: {zip_path}")
        return zip_path

    except HTTPException:
        raise
    except Exception as e:
        log_error(f"Error exporting attachments: {str(e)}")
        raise HTTPException(status_code=500, detail="导出附件失败")

